package xyj.sparrow.scheduler.cron;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;
import java.util.*;

/**
 * cron 表达式
 *
 * @author 江南小俊
 * @since 2021/1/4 15:35
 **/
@Data
@Slf4j
@EqualsAndHashCode
public class CronExpression {
    private String name;
    public static final int SECONDS_END = 59;
    public static final int SECONDS_START = 0;
    public static final int MINUTES_END = 59;
    public static final int MINUTES_START = 0;
    public static final int HOURS_END = 23;
    public static final int HOURS_START = 0;
    public static final int DAYS_OF_MONTH_END = 31;
    public static final int DAYS_OF_MONTH_START = 1;
    public static final int MONTHS_END = 12;
    public static final int MONTHS_START = 1;
    public static final int DAYS_OF_WEEK_END = 7;
    public static final int DAYS_OF_WEEK_START = 1;
    public static final int YEARS_END = 2099;
    public static final int YEARS_START = 1970;

    public static final int SECONDS_TYPE = 0;
    public static final int MINUTES_TYPE = 1;
    public static final int HOURS_TYPE = 2;
    public static final int DAYS_OF_MONTH_TYPE = 3;
    public static final int MONTHS_TYPE = 4;
    private static final int DAYS_OF_WEEK = 5;
    private static final Map<String, Integer> monthMap = new HashMap<>();
    private static final Map<String, Integer> weekMap = new HashMap<>();

    static {
        monthMap.put("JAN", 1);
        monthMap.put("FEB", 2);
        monthMap.put("MAR", 3);
        monthMap.put("APR", 4);
        monthMap.put("MAY", 5);
        monthMap.put("JUN", 6);
        monthMap.put("JUL", 7);
        monthMap.put("AUG", 8);
        monthMap.put("SEP", 9);
        monthMap.put("OCT", 10);
        monthMap.put("NOV", 11);
        monthMap.put("DEC", 12);
    }

    static {
        weekMap.put("SUN", 1);
        weekMap.put("MON", 2);
        weekMap.put("TUE", 3);
        weekMap.put("WED", 4);
        weekMap.put("THU", 5);
        weekMap.put("FRI", 6);
        weekMap.put("SAT", 7);
    }

    /**
     * 0-59 ,- * /
     **/
    private TreeSet<Integer> seconds = new TreeSet<>();
    /**
     * 0-59 ,- * /
     **/
    private TreeSet<Integer> minutes = new TreeSet<>();
    /**
     * 0-23 ,- * /
     **/
    private TreeSet<Integer> hours = new TreeSet<>();
    /**
     * 1-31 ,- * /  LW L W
     **/
    private TreeSet<Integer> daysOfMonth = new TreeSet<>();
    /**
     * 1-12 ,- * /
     **/
    private TreeSet<Integer> months = new TreeSet<>();
    /**
     * 1-7 ,- * / L #
     **/
    private TreeSet<Integer> daysOfWeek = new TreeSet<>();
    /**
     * 1-12 ,- * /
     **/
    private TreeSet<Integer> years = new TreeSet<>();
    /**
     * 本月最后一天
     **/
    private boolean theLastDay = false;
    /**
     * 靠近工作日，不跨月 需前置指定
     **/
    private boolean nearWorkingDay = false;
    /**
     * 本月最后一周 ：周几需前置指定
     **/
    private boolean theLastWeek = false;
    /**
     * 第几个周几，指第几个
     **/
    private int whichWeek = 0;
    /**
     * 下次执行时间
     **/
    private Date nextExecutionTime;

    public CronExpression(String expr, String name) {
        this.name = name;
        try {
            if (StrUtil.hasBlank(expr)) {
                throw new RuntimeException("cron表达式有误");
            }
            String[] exprs = expr.split("\\s+");
            for (int type = 0; type < exprs.length; type++) {
                if (type == SECONDS_TYPE) {
                    parseSeconds(exprs[type]);
                } else if (type == MINUTES_TYPE) {
                    parseMinutes(exprs[type]);
                } else if (type == HOURS_TYPE) {
                    parseHours(exprs[type]);
                } else if (type == DAYS_OF_MONTH_TYPE) {
                    parseDayOfMonth(exprs[type]);
                } else if (type == MONTHS_TYPE) {
                    parseMonth(exprs[type]);
                } else if (type == DAYS_OF_WEEK) {
                    parseDayOfWeek(exprs[type]);
                } else {
                    parseYear(exprs[type]);
                }
            }

        } catch (Exception e) {
            throw new RuntimeException("cron表达式有误");
        }
        getTimeAfter(new Date());
    }

    public CronExpression(String expr) {
        this(expr, IdUtil.simpleUUID());
    }

    private void parseDayOfWeek(String expr) {
        if (daysOfMonth.contains(98)) {
            try {
                if (expr.contains("?") && expr.length() == 1) {
                    daysOfWeek.add(98);
                } else if (expr.indexOf("L") == expr.length() - 1 && expr.length() != 1) {
                    daysOfWeek.add(transform(expr.substring(0, expr.length() - 1), weekMap));
                    theLastWeek = true;
                } else if (expr.contains("#") && expr.length() != 1) {
                    int index = expr.indexOf("#");
                    daysOfWeek.add(transform(expr.substring(index + 1), weekMap));
                    whichWeek = Integer.parseInt(expr.substring(0, index));
                } else {
                    parseDayOfWeek(expr, daysOfWeek, DAYS_OF_WEEK_START, DAYS_OF_WEEK_END);
                }
            } catch (NumberFormatException e) {
                throw new RuntimeException("cron表达式有误");
            }
        } else {
            if (expr.contains("?") && expr.length() == 1) {
                daysOfWeek.add(98);
            } else {
                throw new RuntimeException("不能同时指定日期和周几");
            }
        }

    }

    private void parseDayOfWeek(String expr, TreeSet<Integer> dayOfWeek, int dayOfWeekStart, int dayOfWeekEnd) {
        try {
            if (StrUtil.hasBlank(expr)) {
                throw new RuntimeException("cron表达式有误");
            }
            if (expr.contains("*") && expr.length() == 1) {
                batchAdd(dayOfWeek, dayOfWeekStart, dayOfWeekEnd, 1);
                dayOfWeek.add(99);
            } else if (expr.contains(",")) {
                String[] split = expr.split(",");
                Assert.isTrue(split.length > 1);
                for (int i = 0; i < split.length; i++) {
                    parseDayOfWeek(split[i], dayOfWeek, dayOfWeekStart, dayOfWeekEnd);
                }
            } else if (expr.contains("-") && expr.contains("/")) {
                int first = expr.indexOf("-");
                int second = expr.indexOf("/");
                //1-7/2
                batchAdd(dayOfWeek, transform(expr.substring(0, first), weekMap), transform(expr.substring(first + 1,
                        second), weekMap), transform(expr.substring(second + 1), weekMap));
            } else if (expr.contains("-")) {
                String[] split = expr.split("-");
                batchAdd(dayOfWeek, transform(split[0], weekMap), transform(split[1], weekMap), 1);
            } else if (expr.contains("/")) {
                String[] split = expr.split("/");
                batchAdd(dayOfWeek, transform(split[0], weekMap), dayOfWeekEnd, transform(split[0], weekMap));
            } else {
                validAdd(dayOfWeek, dayOfWeekStart, dayOfWeekEnd, transform(expr, weekMap));
            }
        } catch (NumberFormatException e) {
            throw new RuntimeException("cron表达式有误");
        }
    }

    private void parseYear(String expr) {
        parse(expr, years, YEARS_START, YEARS_END);
    }

    private void parseMonth(String expr) {

        try {
            if (StrUtil.hasBlank(expr)) {
                throw new RuntimeException("cron表达式有误");
            }
            if (expr.contains("*") && expr.length() == 1) {
                batchAdd(months, MONTHS_START, MONTHS_END, 1);
                months.add(99);
            } else if (expr.contains(",")) {
                String[] split = expr.split(",");
                Assert.isTrue(split.length > 1);
                for (int i = 0; i < split.length; i++) {
                    parseMonth(split[i]);
                }
            } else if (expr.contains("-") && expr.contains("/")) {
                int first = expr.indexOf("-");
                int second = expr.indexOf("/");
                //1-7/2
                batchAdd(months, transform(expr.substring(0, first), monthMap), transform(expr.substring(first + 1,
                        second), monthMap), transform(expr.substring(second + 1), monthMap));
            } else if (expr.contains("-")) {
                String[] split = expr.split("-");
                batchAdd(months, transform(split[0], monthMap), transform(split[1], monthMap), 1);
            } else if (expr.contains("/")) {
                String[] split = expr.split("/");
                batchAdd(months, transform(split[0], monthMap), MONTHS_END, transform(split[0], monthMap));
            } else {
                validAdd(months, MONTHS_START, MONTHS_END, transform(expr, monthMap));
            }
        } catch (NumberFormatException e) {
            throw new RuntimeException("cron表达式有误");
        }
    }

    private Integer transform(String str, Map<String, Integer> map) {
        return map.containsKey(str) ? map.get(str) : Integer.parseInt(str);
    }

    private void parseSeconds(String expr) {
        parse(expr, seconds, SECONDS_START, SECONDS_END);
    }

    private void parseMinutes(String expr) {
        parse(expr, minutes, MINUTES_START, MINUTES_END);
    }

    private void parseHours(String expr) {
        parse(expr, hours, HOURS_START, HOURS_END);
    }

    private void parseDayOfMonth(String expr) {
        try {
            if (expr.contains("?") && expr.length() == 1) {
                daysOfMonth.add(98);
            } else if ("LW".equals(expr)) {
                nearWorkingDay = true;
                theLastDay = true;
            } else if (expr.indexOf("W") == expr.length() - 1 && expr.length() != 1) {
                daysOfMonth.add(Integer.valueOf(expr.substring(0, expr.length() - 1)));
                nearWorkingDay = true;
            } else if ("L".equals(expr)) {
                theLastDay = true;
            } else {
                parse(expr, daysOfMonth, DAYS_OF_MONTH_START, DAYS_OF_MONTH_END);
            }
        } catch (NumberFormatException e) {
            throw new RuntimeException("cron表达式有误");
        }
    }

    private void parse(String expr, TreeSet<Integer> treeSet, Integer startInteger, Integer endInteger) {
        try {
            if (StrUtil.hasBlank(expr)) {
                throw new RuntimeException("cron表达式有误");
            }
            if (expr.contains("*") && expr.length() == 1) {
                batchAdd(treeSet, startInteger, endInteger, 1);
                treeSet.add(99);
            } else if (expr.contains(",")) {
                String[] split = expr.split(",");
                Assert.isTrue(split.length > 1);
                for (int i = 0; i < split.length; i++) {
                    parse(split[i], treeSet, startInteger, endInteger);
                }
            } else if (expr.contains("-") && expr.contains("/")) {
                int first = expr.indexOf("-");
                int second = expr.indexOf("/");
                //1-7/2
                batchAdd(treeSet, Integer.parseInt(expr.substring(0, first)),
                        Integer.parseInt(expr.substring(first + 1, second)),
                        Integer.parseInt(expr.substring(second + 1)));
            } else if (expr.contains("-")) {
                String[] split = expr.split("-");
                batchAdd(treeSet, Integer.parseInt(split[0]), Integer.parseInt(split[1]), 1);
            } else if (expr.contains("/")) {
                String[] split = expr.split("/");
                batchAdd(treeSet, Integer.parseInt(split[0]), endInteger, Integer.parseInt(split[1]));
            } else {
                validAdd(treeSet, startInteger, endInteger, Integer.parseInt(expr));
            }
        } catch (NumberFormatException e) {
            throw new RuntimeException("cron表达式有误");
        }
    }

    private void batchAdd(TreeSet<Integer> treeSet, Integer startInteger, Integer endInteger, Integer increment) {
        for (int start = startInteger; start <= endInteger; start += increment) {
            if (rangeDetection(startInteger, endInteger, start)) {
                treeSet.add(start);
            }
        }
    }

    private void validAdd(TreeSet<Integer> treeSet, Integer startInteger, Integer endInteger, int i) {
        if (rangeDetection(startInteger, endInteger, i)) {
            treeSet.add(i);
        }
    }


    private boolean rangeDetection(int start, int end, int target) {
        if (target < start || target > end) {
            throw new RuntimeException("cron表达式有误");
        }
        return true;
    }

    public CronExpression getTimeAfter() {
        nextExecutionTime.setSeconds(nextExecutionTime.getSeconds() + 1);
//        nextExecutionTime.setTime(nextExecutionTime.getTime() + 1);
        getTimeAfter(nextExecutionTime);
        return this;
    }

    public void getTimeAfter(Date date) {
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Calendar calendar = GregorianCalendar.getInstance(TimeZone.getDefault());
//        System.out.println(sdf1.format(calendar.getTime()));
        calendar.setTime(date);
        calendar.set(Calendar.MILLISECOND, 0);
        //取秒
        conversionSecond(calendar);
        //取分
        conversionMinute(calendar);
        //取时
        conversionHour(calendar);
        coversion(calendar);
//        log.info("cron-{}-nextExecutionTime:{}", this.name, sdf1.format(calendar.getTime()));
        nextExecutionTime = calendar.getTime();
    }

    private void coversion(Calendar calendar) {
        //取日
        conversionDayOfMonth(calendar);
        conversionDayOfWeek(calendar);
        //取月
        conversionMonth(calendar);
        //取年
        conversionYear(calendar);
    }

    /**
     * @param calendar
     * @return void
     * @description 转换年
     * @date 2021/1/8 17:13
     * @author Jim
     **/
    private void conversionYear(Calendar calendar) {
        if (years.isEmpty()) {
            return;
        }
        SortedSet<Integer> yearsSet = years.tailSet(calendar.get(Calendar.YEAR));
        if (yearsSet.isEmpty()) {
            throw new RuntimeException("没有下一次执行时间");
        } else if (yearsSet.first() != calendar.get(Calendar.YEAR)) {
            calendar.set(Calendar.YEAR, yearsSet.first());
            //强制重置时分秒 日 月
            reset(1, calendar, true);
            calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMinimum(Calendar.DAY_OF_MONTH));
            calendar.set(Calendar.MONTH, calendar.getActualMinimum(Calendar.MONTH));
            coversion(calendar);
        }
    }

    /**
     * @param calendar
     * @return void
     * @description 转换月
     * @date 2021/1/8 17:08
     * @author Jim
     **/
    private void conversionMonth(Calendar calendar) {
        SortedSet<Integer> monthsSet = months.tailSet(calendar.get(Calendar.MONTH) + 1);
        if (monthsSet.isEmpty()) {
            calendar.set(Calendar.MONTH, months.first());
            calendar.add(Calendar.YEAR, 1);
        } else if (monthsSet.first() != calendar.get(Calendar.MONTH) + 1) {
            calendar.set(Calendar.MONTH, monthsSet.first() - 1);
            //强制重置时分秒 日
            reset(1, calendar, true);
            calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMinimum(Calendar.DAY_OF_MONTH));
            coversion(calendar);
        }
    }

    /**
     * @param calendar
     * @return void
     * @description 转换 时
     * @date 2021/1/8 17:05
     * @author Jim
     **/
    private void conversionHour(Calendar calendar) {
        SortedSet<Integer> hoursSet = hours.tailSet(calendar.get(Calendar.HOUR_OF_DAY));
        if (hoursSet.isEmpty()) {
            calendar.set(Calendar.HOUR_OF_DAY, hours.first());
            calendar.add(Calendar.DAY_OF_MONTH, 1);
        } else {
            calendar.set(Calendar.HOUR_OF_DAY, hoursSet.first());
        }
    }

    /**
     * @param calendar
     * @return void
     * @description 转换分
     * @date 2021/1/8 17:06
     * @author Jim
     **/
    private void conversionMinute(Calendar calendar) {
        SortedSet<Integer> minutesSet = minutes.tailSet(calendar.get(Calendar.MINUTE));
        if (minutesSet.isEmpty()) {
            calendar.set(Calendar.MINUTE, minutes.first());
            calendar.add(Calendar.HOUR_OF_DAY, 1);
        } else {
            calendar.set(Calendar.MINUTE, minutesSet.first());
        }
    }

    /**
     * @param calendar
     * @return void
     * @description 转换秒
     * @date 2021/1/8 17:06
     * @author Jim
     **/
    private void conversionSecond(Calendar calendar) {
        SortedSet<Integer> secondsSet = seconds.tailSet(calendar.get(Calendar.SECOND));
        if (secondsSet.isEmpty()) {
            calendar.set(Calendar.SECOND, seconds.first());
            calendar.add(Calendar.MINUTE, 1);
        } else {
            calendar.set(Calendar.SECOND, secondsSet.first());
        }
    }

    /**
     * @param calendar
     * @return void
     * @description 转换日
     * @date 2021/1/8 17:06
     * @author Jim
     **/
    private void conversionDayOfMonth(Calendar calendar) {
        if (daysOfMonth.contains(98)) {
            return;
        }
        int day = calendar.get(Calendar.DAY_OF_MONTH);//今天
        //取日
        if (theLastDay && nearWorkingDay) {//最后一个工作日
            calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
            switch (calendar.get(Calendar.DAY_OF_WEEK)) {
                case 1:
                    calendar.add(Calendar.DAY_OF_MONTH, -2);
                case 7:
                    calendar.add(Calendar.DAY_OF_MONTH, -1);
            }

        } else if (theLastDay) {//最后一天
            calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
        } else if (nearWorkingDay) {//靠近哪天的工作日
            calendar.set(Calendar.DAY_OF_MONTH, daysOfMonth.first());
            switch (calendar.get(Calendar.DAY_OF_WEEK)) {
                case 1://周日
                    //正常是+1
                    if (calendar.getActualMaximum(Calendar.DAY_OF_MONTH) >= calendar.get(Calendar.DAY_OF_MONTH) + 1) {
                        calendar.add(Calendar.DAY_OF_MONTH, 1);
                    } else {
                        calendar.add(Calendar.DAY_OF_MONTH, -2);
                    }

                    ;
                    break;
                case 7://周六
                    //正常是减1
                    if (calendar.get(Calendar.DAY_OF_MONTH) > 1) {
                        calendar.add(Calendar.DAY_OF_MONTH, -1);
                    } else {
                        calendar.add(Calendar.DAY_OF_MONTH, 2);
                    }
                    ;
                    break;


            }
//            calendar.set();
        } else {
            //取时
            SortedSet<Integer> daysSet = daysOfMonth.tailSet(calendar.get(Calendar.DAY_OF_MONTH));
            if (daysSet.isEmpty()) {
                calendar.set(Calendar.DAY_OF_MONTH, daysOfMonth.first());
                calendar.add(Calendar.MONTH, 1);
                //强制重置时分秒
                reset(day, calendar, true);

            } else {
                calendar.set(Calendar.DAY_OF_MONTH, daysSet.first());
            }
        }
        reset(day, calendar, false);
    }

    /**
     * @param calendar
     * @return void
     * @description 转换周
     * @date 2021/1/8 17:07
     * @author Jim
     **/
    private void conversionDayOfWeek(Calendar calendar) {
        if (daysOfWeek.contains(98)) {
            return;
        }
        int day = calendar.get(Calendar.DAY_OF_MONTH);//今天
        //
        if (theLastWeek) {//最后一个周几
            calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
            calendar.add(Calendar.DAY_OF_MONTH, -(calendar.get(Calendar.DAY_OF_WEEK) + 7 - daysOfWeek.first()) % 7);
        } else {//周几
            int week = calendar.get(Calendar.DAY_OF_WEEK);
            Integer targetWeek = daysOfWeek.first();
            if (week < targetWeek) {
                calendar.add(Calendar.DAY_OF_MONTH, targetWeek - week);
            } else if (week > targetWeek) {
                calendar.add(Calendar.DAY_OF_MONTH, targetWeek + 7 - week);
            }
        }
        reset(day, calendar, false);

    }

    private void reset(int day, Calendar calendar, boolean forcibly) {
        if (day != calendar.get(Calendar.DAY_OF_MONTH) || forcibly) {
            calendar.set(Calendar.SECOND, seconds.first());
            calendar.set(Calendar.MINUTE, minutes.first());
            calendar.set(Calendar.HOUR_OF_DAY, hours.first());
        }
    }
}
